home *** CD-ROM | disk | FTP | other *** search
/ Total Network Tools 2002 / NextStepPublishing-TotalNetworkTools2002-Win95.iso / Archive / Misc Servers / Zope.exe / FTPSERVER.PY < prev    next >
Encoding:
Python Source  |  2000-08-16  |  23.6 KB  |  635 lines

  1. ##############################################################################
  2. # Zope Public License (ZPL) Version 1.0
  3. # -------------------------------------
  4. # Copyright (c) Digital Creations.  All rights reserved.
  5. # This license has been certified as Open Source(tm).
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions are
  8. # met:
  9. # 1. Redistributions in source code must retain the above copyright
  10. #    notice, this list of conditions, and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. #    notice, this list of conditions, and the following disclaimer in
  13. #    the documentation and/or other materials provided with the
  14. #    distribution.
  15. # 3. Digital Creations requests that attribution be given to Zope
  16. #    in any manner possible. Zope includes a "Powered by Zope"
  17. #    button that is installed by default. While it is not a license
  18. #    violation to remove this button, it is requested that the
  19. #    attribution remain. A significant investment has been put
  20. #    into Zope, and this effort will continue if the Zope community
  21. #    continues to grow. This is one way to assure that growth.
  22. # 4. All advertising materials and documentation mentioning
  23. #    features derived from or use of this software must display
  24. #    the following acknowledgement:
  25. #      "This product includes software developed by Digital Creations
  26. #      for use in the Z Object Publishing Environment
  27. #      (http://www.zope.org/)."
  28. #    In the event that the product being advertised includes an
  29. #    intact Zope distribution (with copyright and license included)
  30. #    then this clause is waived.
  31. # 5. Names associated with Zope or Digital Creations must not be used to
  32. #    endorse or promote products derived from this software without
  33. #    prior written permission from Digital Creations.
  34. # 6. Modified redistributions of any form whatsoever must retain
  35. #    the following acknowledgment:
  36. #      "This product includes software developed by Digital Creations
  37. #      for use in the Z Object Publishing Environment
  38. #      (http://www.zope.org/)."
  39. #    Intact (re-)distributions of any official Zope release do not
  40. #    require an external acknowledgement.
  41. # 7. Modifications are encouraged but must be packaged separately as
  42. #    patches to official Zope releases.  Distributions that do not
  43. #    clearly separate the patches from the original work must be clearly
  44. #    labeled as unofficial distributions.  Modifications which do not
  45. #    carry the name Zope may be packaged in any form, as long as they
  46. #    conform to all of the clauses above.
  47. # Disclaimer
  48. #   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
  49. #   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  50. #   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  51. #   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
  52. #   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  53. #   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  54. #   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  55. #   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  56. #   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  57. #   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  58. #   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  59. #   SUCH DAMAGE.
  60. # This software consists of contributions made by Digital Creations and
  61. # many individuals on behalf of Digital Creations.  Specific
  62. # attributions are listed in the accompanying credits file.
  63. ##############################################################################
  64.  
  65. """ZServer FTP Channel for use the medusa's ftp server.
  66.  
  67. FTP Service for Zope.
  68.  
  69.   This server allows FTP connections to Zope. In general FTP is used
  70.   to manage content. You can:
  71.  
  72.     * Create and delete Folders, Documents, Files, and Images
  73.  
  74.     * Edit the contents of Documents, Files, Images
  75.  
  76.   In the future, FTP may be used to edit object properties.
  77.  
  78. FTP Protocol
  79.  
  80.   The FTP protocol for Zope gives Zope objects a way to make themselves
  81.   available to FTP services. See the 'lib/python/OFS/FTPInterface.py' for
  82.   more details.
  83.  
  84. FTP Permissions
  85.  
  86.   FTP access is controlled by one permission: 'FTP access' if bound to a
  87.   role, users of that role will be able to list directories, and cd to
  88.   them. Creating and deleting and changing objects are all governed by
  89.   existing Zope permissions.
  90.  
  91.   Permissions are to a certain extent reflected in the permission bits
  92.   listed in FTP file listings.
  93.  
  94. FTP Authorization
  95.  
  96.   Zope supports both normal and anonymous logins. It can be difficult
  97.   to authorize Zope users since they are defined in distributed user
  98.   databases. Normally, all logins will be accepted and then the user must
  99.   proceed to 'cd' to a directory in which they are authorized. In this
  100.   case for the purpose of FTP limits, the user is considered anonymous
  101.   until they cd to an authorized directory.
  102.  
  103.   Optionally, users can login with a special username which indicates
  104.   where they are defined. Their login will then be authenticated in
  105.   the indicated directory, and they will not be considered anonymous.
  106.   The form of the name is '<username>@<path>' where path takes the form
  107.   '<folder id>[/<folder id>...]' For example: 'amos@Foo/Bar' This will
  108.   authenticate the user 'amos' in the directory '/Foo/Bar'. In addition
  109.   the user's FTP session will be rooted in the authenticated directory,
  110.   i.e. they will not be able to cd out of the directory.
  111.  
  112.   The main reason to use the rooted FTP login, is to allow non-anonymous
  113.   logins. This may be handy, if for example, you disallow anonymous logins,
  114.   or if you set the limit for simultaneous anonymous logins very low.
  115.  
  116. """
  117.  
  118. from PubCore import handle
  119. from medusa.ftp_server import ftp_channel, ftp_server, recv_channel
  120. from medusa import asyncore, asynchat, filesys
  121. from FTPResponse import make_response
  122. from FTPRequest import FTPRequest
  123.  
  124. from ZServer import CONNECTION_LIMIT
  125.  
  126. from cStringIO import StringIO
  127. import string
  128. import os
  129. from mimetypes import guess_type
  130. import marshal
  131. import stat
  132. import time
  133.  
  134.  
  135. class zope_ftp_channel(ftp_channel):
  136.     "Passes its commands to Zope, not a filesystem"
  137.     
  138.     read_only=0
  139.     anonymous=1
  140.     
  141.     def __init__ (self, server, conn, addr, module):
  142.         ftp_channel.__init__(self,server,conn,addr)
  143.         self.module=module
  144.         self.userid=''
  145.         self.password=''
  146.         self.path='/'
  147.         self.cookies={}
  148.     
  149.     def _join_paths(self,*args):
  150.         path=apply(os.path.join,args)
  151.         path=os.path.normpath(path)
  152.         if os.sep != '/':
  153.             path=string.replace(path,os.sep,'/')
  154.         return path
  155.     
  156.     # Overriden async_chat methods
  157.  
  158.     def push(self, producer, send=1):
  159.         # this is thread-safe when send is false
  160.         # note, that strings are not wrapped in 
  161.         # producers by default
  162.         self.producer_fifo.push(producer)
  163.         if send: self.initiate_send()
  164.         
  165.     push_with_producer=push
  166.     
  167.     
  168.     # Overriden ftp_channel methods
  169.  
  170.     def cmd_nlst (self, line):
  171.         'give name list of files in directory'
  172.         self.get_dir_list(line,0)
  173.  
  174.     def cmd_list (self, line):
  175.         'give list files in a directory'
  176.         
  177.         # handles files as well as directories.
  178.         # XXX also should maybe handle globbing, yuck.
  179.   
  180.         self.get_dir_list(line,1)
  181.     
  182.     def get_dir_list(self, line, long=0):
  183.         # we need to scan the command line for arguments to '/bin/ls'...
  184.         # XXX clean this up, maybe with getopts
  185.         if len(line) > 1:
  186.             args = string.split(line[1])
  187.         else:
  188.             args =[]
  189.         path_args = []
  190.         for arg in args:
  191.             if arg[0] != '-':
  192.                 path_args.append (arg)
  193.             else:
  194.                 if 'l' in arg:
  195.                     long=1
  196.         if len(path_args) < 1:
  197.             dir = '.'
  198.         else:
  199.             dir = path_args[0]
  200.         self.listdir(dir, long)
  201.     
  202.     def listdir (self, path, long=0):
  203.         response=make_response(self, self.listdir_completion, long)
  204.         request=FTPRequest(path, 'LST', self, response)
  205.         handle(self.module, request, response)         
  206.         
  207.     def listdir_completion(self, long, response):
  208.         status=response.getStatus()
  209.         if status==200:
  210.             if self.anonymous and not self.userid=='anonymous':
  211.                 self.anonymous=None
  212.             dir_list=''
  213.             file_infos=response._marshalledBody()
  214.             if type(file_infos[0])==type(''):
  215.                 file_infos=(file_infos,)    
  216.             if long:
  217.                 for id, stat_info in file_infos:
  218.                     dir_list=dir_list+filesys.unix_longify(id,stat_info)+'\r\n'
  219.             else:
  220.                 for id, stat_info in file_infos:
  221.                     dir_list=dir_list+id+'\r\n'
  222.             self.make_xmit_channel()
  223.             self.client_dc.push(dir_list)
  224.             self.client_dc.close_when_done()                
  225.             self.respond(
  226.                 '150 Opening %s mode data connection for file list' % (
  227.                     self.type_map[self.current_mode]
  228.                     )
  229.                 )   
  230.         elif status==401:
  231.             self.respond('530 Unauthorized.')
  232.         else:
  233.             self.respond('550 Could not list directory.')
  234.  
  235.     def cmd_cwd (self, line):
  236.         'change working directory'
  237.         response=make_response(self, self.cwd_completion, 
  238.                 self._join_paths(self.path,line[1]))
  239.         request=FTPRequest(line[1],'CWD',self,response)
  240.         handle(self.module,request,response)       
  241.  
  242.     def cwd_completion(self,path,response):
  243.         'cwd completion callback'
  244.         status=response.getStatus()
  245.         if status==200:
  246.             listing=response._marshalledBody()
  247.             # check to see if we are cding to a non-foldoid object
  248.             if type(listing[0])==type(''):
  249.                 self.respond('550 No such directory.')
  250.                 return
  251.             else:
  252.                 self.path=path or '/'
  253.                 self.respond('250 CWD command successful.')
  254.                 # now that we've sucussfully cd'd perhaps we are no
  255.                 # longer anonymous
  256.                 if self.anonymous and not self.userid=='anonymous':
  257.                     self.anonymous=None
  258.         elif status==401:
  259.             self.respond('530 Unauthorized.')
  260.         else:
  261.             self.respond('550 No such directory.')
  262.  
  263.     def cmd_cdup (self, line):
  264.         'change to parent of current working directory'
  265.         self.cmd_cwd((None,'..'))
  266.  
  267.     def cmd_pwd (self, line):
  268.         'print the current working directory'
  269.         self.respond (
  270.             '257 "%s" is the current directory.' % (
  271.                 self.path
  272.                 )
  273.             )
  274.     
  275.     cmd_xpwd=cmd_pwd
  276.     
  277.     def cmd_mdtm(self, line):
  278.         'show last modification time of file'
  279.         if len (line) != 2:
  280.             self.command.not_understood (string.join (line))
  281.             return
  282.         response=make_response(self, self.mdtm_completion)
  283.         request=FTPRequest(line[1],'MDTM',self,response)
  284.         handle(self.module,request,response)       
  285.     
  286.     def mdtm_completion(self, response):
  287.         status=response.getStatus()
  288.         if status==200:
  289.             mtime=response._marshalledBody()[stat.ST_MTIME]
  290.             mtime=time.gmtime(mtime)
  291.             self.respond('213 %4d%02d%02d%02d%02d%02d' % (
  292.                                 mtime[0],
  293.                                 mtime[1],
  294.                                 mtime[2],
  295.                                 mtime[3],
  296.                                 mtime[4],
  297.                                 mtime[5]
  298.                                 ))
  299.         elif status==401:
  300.             self.respond('530 Unauthorized.') 
  301.         else:
  302.             self.respond('550 Error getting file modification time.')  
  303.                 
  304.     def cmd_size(self, line):
  305.         'return size of file'
  306.         if len (line) != 2:
  307.             self.command.not_understood (string.join (line))
  308.             return
  309.         response=make_response(self, self.size_completion)
  310.         request=FTPRequest(line[1],'SIZE',self,response)
  311.         handle(self.module,request,response)       
  312.         
  313.     def size_completion(self,response):
  314.         status=response.getStatus()
  315.         if status==200:
  316.             self.respond('213 %d'% response._marshalledBody()[stat.ST_SIZE])
  317.         elif status==401:
  318.             self.respond('530 Unauthorized.') 
  319.         else:
  320.             self.respond('550 Error getting file size.')   
  321.             
  322.             #self.client_dc.close_when_done()
  323.  
  324.     def cmd_retr(self,line):
  325.         if len(line) < 2:
  326.             self.command_not_understood (string.join (line))
  327.             return
  328.         response=make_response(self, self.retr_completion, line[1])
  329.         self._response_producers = response.stdout._producers
  330.         request=FTPRequest(line[1],'RETR',self,response)
  331.         handle(self.module,request,response) 
  332.  
  333.     def retr_completion(self, file, response):        
  334.         status=response.getStatus()
  335.         if status==200:
  336.             self.make_xmit_channel()
  337.             if not response._wrote:
  338.                 self.client_dc.push(response.body)
  339.             else:
  340.                 for producer in self._response_producers:
  341.                     self.client_dc.push_with_producer(producer)
  342.             self._response_producers = None
  343.             self.client_dc.close_when_done()
  344.             self.respond(
  345.                     "150 Opening %s mode data connection for file '%s'" % (
  346.                         self.type_map[self.current_mode],
  347.                         file
  348.                         ))
  349.         elif status==401:
  350.             self.respond('530 Unauthorized.')
  351.         else:
  352.             self.respond('550 Error opening file.')    
  353.  
  354.     def cmd_stor (self, line, mode='wb'):
  355.         'store a file'
  356.         if len (line) < 2:
  357.             self.command_not_understood (string.join (line))
  358.             return
  359.         elif self.restart_position:
  360.             restart_position = 0
  361.             self.respond ('553 restart on STOR not yet supported')
  362.             return
  363.             
  364.         # XXX Check for possible problems first?
  365.         #     Right now we are limited in the errors we can issue, since
  366.         #     we agree to accept the file before checking authorization
  367.         
  368.         fd=ContentReceiver(self.stor_callback, line[1])
  369.         self.respond (
  370.             '150 Opening %s connection for %s' % (
  371.                 self.type_map[self.current_mode],
  372.                 line[1]
  373.                 )
  374.             )
  375.         self.make_recv_channel(fd)
  376.     
  377.     def stor_callback(self,path,data):
  378.         'callback to do the STOR, after we have the input'
  379.         response=make_response(self, self.stor_completion)
  380.         request=FTPRequest(path,'STOR',self,response,stdin=data)
  381.         handle(self.module,request,response)       
  382.            
  383.     def stor_completion(self,response):
  384.         status=response.getStatus()        
  385.         if status in (200,201,204,302):
  386.             self.client_dc.channel.respond('226 Transfer complete.')
  387.         elif status==401:
  388.             self.client_dc.channel.respond('426 Unauthorized.')
  389.         else:
  390.             self.client_dc.channel.respond('426 Error creating file.')       
  391.         self.client_dc.close()
  392.         
  393.     def cmd_dele(self, line):
  394.         if len (line) != 2:
  395.             self.command.not_understood (string.join (line))
  396.             return
  397.         path,id=os.path.split(line[1])
  398.         response=make_response(self, self.dele_completion)
  399.         request=FTPRequest(path,('DELE',id),self,response)
  400.         handle(self.module,request,response)       
  401.         
  402.     def dele_completion(self,response):   
  403.         status=response.getStatus()
  404.         if status==200 and string.find(response.body,'Not Deletable')==-1:
  405.             self.respond('250 DELE command successful.')
  406.         elif status==401:
  407.             self.respond('530 Unauthorized.') 
  408.         else:
  409.             self.respond('550 Error deleting file.')       
  410.  
  411.     def cmd_mkd(self, line):
  412.         if len (line) != 2:
  413.             self.command.not_understood (string.join (line))
  414.             return
  415.         path,id=os.path.split(line[1])
  416.         response=make_response(self, self.mkd_completion)
  417.         request=FTPRequest(path,('MKD',id),self,response)
  418.         handle(self.module,request,response)       
  419.     
  420.     cmd_xmkd=cmd_mkd
  421.     
  422.     def mkd_completion(self,response):
  423.         status=response.getStatus()
  424.         if status==200:
  425.             self.respond('257 MKD command successful.')
  426.         elif status==401:
  427.             self.respond('530 Unauthorized.')
  428.         else:
  429.             self.respond('550 Error creating directory.')
  430.  
  431.     def cmd_rmd(self, line):
  432.         # XXX should object be checked to see if it's folderish
  433.         #     before we allow it to be RMD'd?
  434.         if len (line) != 2:
  435.             self.command.not_understood (string.join (line))
  436.             return
  437.         path,id=os.path.split(line[1])
  438.         response=make_response(self, self.rmd_completion)
  439.         request=FTPRequest(path,('RMD',id),self,response)
  440.         handle(self.module,request,response)       
  441.         
  442.     cmd_xrmd=cmd_rmd
  443.  
  444.     def rmd_completion(self,response):
  445.         status=response.getStatus()
  446.         if status==200 and string.find(response.body,'Not Deletable')==-1:
  447.             self.respond('250 RMD command successful.')
  448.         elif status==401:
  449.             self.respond('530 Unauthorized.') 
  450.         else:
  451.             self.respond('550 Error removing directory.')          
  452.  
  453.     def cmd_user(self, line):
  454.         'specify user name'
  455.         if len(line) > 1:
  456.             self.userid = line[1]
  457.             self.respond('331 Password required.')
  458.         else:
  459.             self.command_not_understood (string.join (line))
  460.  
  461.     def cmd_pass(self, line):
  462.         'specify password'
  463.         if len(line) < 2:
  464.             pw = ''
  465.         else:
  466.             pw = line[1]
  467.         self.password=pw
  468.         i=string.find(self.userid,'@')
  469.         if i ==-1:
  470.             if self.server.limiter.check_limit(self):
  471.                 self.respond ('230 Login successful.')
  472.                 self.authorized = 1
  473.                 self.anonymous = 1
  474.                 self.log_info ('Successful login.')
  475.             else:
  476.                 self.respond('421 User limit reached. Closing connection.')
  477.                 self.close_when_done()
  478.         else:   
  479.             path=self.userid[i+1:]
  480.             self.userid=self.userid[:i]
  481.             self.anonymous=None
  482.             response=make_response(self, self.pass_completion,
  483.                     self._join_paths('/',path))
  484.             request=FTPRequest(path,'PASS',self,response)
  485.             handle(self.module,request,response) 
  486.  
  487.  
  488.     def pass_completion(self,path,response):
  489.         status=response.getStatus()
  490.         if status==200:
  491.             if not self.server.limiter.check_limit(self):
  492.                 self.close_when_done()
  493.                 self.respond('421 User limit reached. Closing connection.')    
  494.                 return
  495.             listing=response._marshalledBody()
  496.             # check to see if we are cding to a non-foldoid object
  497.             if type(listing[0])==type(''):
  498.                 self.respond('530 Unauthorized.')
  499.                 return
  500.             self.path=path or '/'
  501.             self.authorized = 1
  502.             if self.userid=='anonymous':
  503.                 self.anonymous=1
  504.             self.log_info('Successful login.')           
  505.             self.respond('230 Login successful.')
  506.         else:
  507.             self.respond('530 Unauthorized.')
  508.         
  509.     def cmd_appe(self, line):
  510.         self.respond('502 Command not implemented.')
  511.  
  512.  
  513. # Override ftp server receive channel reponse mechanism 
  514. # XXX hack alert, this should probably be redone in a more OO way.
  515.  
  516. def handle_close (self):
  517.     """response and closure of channel is delayed."""
  518.     s = self.channel.server
  519.     s.total_files_in.increment()
  520.     s.total_bytes_in.increment(self.bytes_in.as_long())
  521.     self.fd.close()
  522.     self.readable=lambda :0 # don't call close again
  523.  
  524. recv_channel.handle_close=handle_close
  525.  
  526.   
  527. class ContentReceiver:
  528.     "Write-only file object used to receive data from FTP"
  529.     
  530.     def __init__(self,callback,*args):
  531.         self.data=StringIO()
  532.         self.callback=callback
  533.         self.args=args
  534.         
  535.     def write(self,data):
  536.         self.data.write(data)
  537.     
  538.     def close(self):
  539.         self.data.seek(0)
  540.         args=self.args+(self.data,)
  541.         c=self.callback
  542.         self.callback=None
  543.         self.args=None
  544.         apply(c,args)
  545.  
  546.  
  547. class FTPLimiter:
  548.     """Rudimentary FTP limits. Helps prevent denial of service
  549.     attacks. It works by limiting the number of simultaneous
  550.     connections by userid. There are three limits, one for anonymous
  551.     connections, and one for authenticated logins. The total number
  552.     of simultaneous anonymous logins my be less than or equal to the
  553.     anonymous limit. Each authenticated user can have up to the user
  554.     limit number of simultaneous connections. The total limit is the
  555.     maximum number of simultaneous connections of any sort. Do *not*
  556.     set the total limit lower than or equal to the anonymous limit."""
  557.     
  558.     def __init__(self,anon_limit=10,user_limit=4,total_limit=25):
  559.         self.anon_limit=anon_limit
  560.         self.user_limit=user_limit
  561.         self.total_limit=total_limit
  562.     
  563.     def check_limit(self,channel):
  564.         """Check to see if the user has exhausted their limit or not.
  565.         Check for existing channels with the same userid and the same
  566.         ftp server."""
  567.         total=0
  568.         class_total=0
  569.         if channel.anonymous:
  570.             for existing_channel in asyncore.socket_map.values():
  571.                 if (hasattr(existing_channel,'server') and
  572.                         existing_channel.server is channel.server):
  573.                     total=total+1
  574.                     if existing_channel.anonymous:
  575.                         class_total=class_total+1
  576.             if class_total > self.anon_limit:
  577.                 return None
  578.         else:                
  579.             for existing_channel in asyncore.socket_map.values():
  580.                 if (hasattr(existing_channel,'server') and
  581.                         existing_channel.server is channel.server):
  582.                     total=total+1
  583.                     if channel.userid==existing_channel.userid:
  584.                         class_total=class_total+1
  585.             if class_total > self.user_limit:
  586.                 return None
  587.         if total <= self.total_limit:
  588.             return 1
  589.  
  590.         
  591. class FTPServer(ftp_server):
  592.     """FTP server for Zope."""
  593.     
  594.     ftp_channel_class = zope_ftp_channel
  595.     limiter=FTPLimiter(10,1)
  596.  
  597.     def __init__(self,module,*args,**kw):
  598.         apply(ftp_server.__init__, (self, None) + args, kw)
  599.         self.module=module
  600.         
  601.     def handle_accept (self):
  602.         conn, addr = self.accept()
  603.         self.total_sessions.increment()
  604.         self.log_info('Incoming connection from %s:%d' % (addr[0], addr[1]))
  605.         self.ftp_channel_class (self, conn, addr, self.module)  
  606.  
  607.     def readable(self):
  608.         return len(asyncore.socket_map) < CONNECTION_LIMIT
  609.  
  610.     def listen(self, num):
  611.         # override asyncore limits for nt's listen queue size
  612.         self.accepting = 1
  613.         return self.socket.listen (num)
  614.